using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.IO;
// good parser :P
namespace DelphiBinaryObjectParser
{
    public class DelphiParser
    {
        Stream strm;
        List<DelphiCustomParser> parsers = new List<DelphiCustomParser>();

        public DelphiObject parseBinaryFile(String path)
        {
            return parseBinaryFile(new FileInfo(path).OpenRead());
        }
        public DelphiObject parseBinaryFile(Stream s)
        {
            DelphiObject returnObject = new DelphiObject();
            strm = s;
            byte[] sigdwordBufferf = new byte[4];
            // start at beginning of stream
            strm.Seek(0, SeekOrigin.Begin);

            // verify signature
            strm.Read(sigdwordBufferf, 0, 4);
            if (BitConverter.ToInt32(sigdwordBufferf, 0) != 0x30465054)
            {
                throw new InvalidDataException("This doesn't seem to be a Binary Delphi Object");
            }
            returnObject = parseDelphiObject();
            return returnObject;
        }
        public void addCustomParser(DelphiCustomParser p)
        {
            parsers.Add(p);
        }
        // object level
        public DelphiObject parseDelphiObject()
        {
            DelphiObject returnObject = new DelphiObject();
            returnObject.name = getNextPropertyName();
            //strm.Seek(1, SeekOrigin.Current);
            returnObject.extends = getNextPropertyName();
            if (returnObject.extends != "")
            {
                string tmp = returnObject.name;
                returnObject.name = returnObject.extends;
                returnObject.extends = tmp;
            }
            while (peekNextByte() != 0 )
            {
                if (strm.Position >= strm.Length-1)
                    break;
                returnObject.properties.Add(getNextProperty());

            }
            strm.ReadByte();
            while (peekNextByte() != 0  )
            {
                if (strm.Position >= strm.Length - 1)
                    break;
                returnObject.objects.Add(parseDelphiObject());
            }
            strm.ReadByte();
            return returnObject;
        }

        // property level
        public DelphiProperty getNextProperty()
        {
            DelphiProperty prop = new DelphiProperty();
            prop.name = getNextPropertyName();
            foreach (var v in parsers)
            {
                if (v.shouldParse(prop.name))
                {
                    prop.type = -1;
                    prop.value = v.parse(this);
                }
            }
            if (prop.type != -1)
            {
                prop.type = getPropertyType();
                prop.value = getPropertyValue(prop.type);
            }
            return prop;
        }
        public String getNextPropertyName()
        {
            return readPascalString();
        }
        public int getPropertyType()
        {
            return strm.ReadByte();
        }
        public Object getPropertyValue(int type)
        {
            byte[] dwordBuffer = new byte[4];
            switch (type)
            {
                case 1:
                    // dealing with an array
                    ArrayList list = new ArrayList();
                    int elemType = getPropertyType();
                    while (elemType != 0)
                    {
                        list.Add(getPropertyValue(elemType));
                        elemType = getPropertyType();
                    }
                    return list;

                case 2:
                    return Convert.ToInt32((SByte)strm.ReadByte());
                case 3:
                    strm.Read(dwordBuffer, 0, 2);
                    return Convert.ToInt32((Int16)BitConverter.ToInt16(dwordBuffer, 0));
                case 4:
                    strm.Read(dwordBuffer, 0, 4);
                    return BitConverter.ToInt32(dwordBuffer, 0);
                case 5:
                    byte[] extendedBuff = new byte[10];
                    strm.Read(extendedBuff, 0, 10);
                    double result = ExtendedToDouble(extendedBuff);
                    return result;
                case 6:
                    return readPascalString();
                case 7:
                    //throw new NotSupportedException("This data type is not yet supported");
                    return readPascalString();
                case 8:
                    return false;
                case 9:
                    return true;
                case 0x0A:
                    // hex data returned as byte[]
                    int numBytes = strm.ReadByte();
                    strm.Seek(3, SeekOrigin.Current);
                    byte[] buf = new byte[numBytes];
                    strm.Read(buf, 0, numBytes);
                    return buf;
                case 0x0B:
                    List<String> set = new List<String>();
                    String nextString;
                    while (peekNextByte() != 0)
                    {
                        nextString = readPascalString();
                        set.Add(nextString);
                    }
                    // skip this 00 bytes
                    strm.ReadByte();
                    return set;
                case 0x0C:
                    return readLongPascalString();
                case 0x0D:
                    return null;
                case 0x0E:
                    ArrayList collection = new ArrayList();
                    int anotherItem = strm.ReadByte();
                    while (anotherItem == 1)
                    {
                        ArrayList item = new ArrayList();
                        while (peekNextByte() != 0)
                        {
                            DelphiProperty tmpProp = new DelphiProperty();
                            tmpProp = getNextProperty();
                            item.Add(tmpProp); 
                        }
                        strm.ReadByte();
                        collection.Add(item);
                        anotherItem = strm.ReadByte();
                    }
                    return collection;
                    // crazy shits :)
                case 0x11:
                    byte[] doubledwordBufferf = new byte[8];
                    strm.Read(doubledwordBufferf, 0, 8);
                    double result2 = BitConverter.ToDouble(doubledwordBufferf, 0);
                    return result2;
                case 0x12:
                    strm.Seek(4, SeekOrigin.Current);
                    return "";
                default:
                    return null;
                    //throw new NotSupportedException("Data type: " + String.Format("{0:x}", type) + " is not supported yet!");

            }
        }
        
        // helpers
        public int peekNextByte()
        {
            int ret = strm.ReadByte();
            strm.Seek(-1, SeekOrigin.Current);
            return ret;
        }
        public String readPascalString()
        {
            int strLength = strm.ReadByte();
            byte[] strBuffer = new byte[strLength];
            strm.Read(strBuffer, 0, strLength);

            return Encoding.ASCII.GetString(strBuffer);
        }
        public String readLongPascalString()
        {
            byte[] dwordBuffer = new byte[4];
            int strLen;
            strm.Read(dwordBuffer, 0, 4);
            strLen = BitConverter.ToInt32(dwordBuffer, 0);
            byte[] strBuffer = new byte[strLen];
            strm.Read(strBuffer, 0, strLen);
            return System.Text.ASCIIEncoding.ASCII.GetString(strBuffer);
        }


        public double ExtendedToDouble(byte[] buffer)
        {
            double sign = ((buffer[9] & 0x80) == 0x00) ? 1 : -1;

            UInt32 exponent = (((UInt32)buffer[9] & 0x7f) << 8) | (UInt32)buffer[8];

            UInt64 mantissa = BitConverter.ToUInt64(buffer, 0);
            double normalizeCorrection;
            if ((mantissa & 0x8000000000000000) != 0x00)
                normalizeCorrection = 1;
            else
                normalizeCorrection = 0;
            mantissa &= 0x7FFFFFFFFFFFFFFF;



            return (sign * (normalizeCorrection + (mantissa / Math.Pow(2,63)))) * ( Math.Pow(2,(Int32)exponent - 16383));
        }
    }

    public class DelphiObject
    {
        public string name { get; set; }
        public string extends { get; set; }
        public List<DelphiProperty> properties = new List<DelphiProperty>();
        public List<DelphiObject> objects = new List<DelphiObject>();

        public DelphiProperty getPropertyByName(string str)
        {
            foreach (var p in properties)
            {
                if (p.name.Trim() == str) {
                    return p; 
                }
            }
            return null;
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("object " + name + (extends != "" ? " : " + extends : ""));
            foreach (var v in properties)
            {
                if (v.type != -1)
                {
                    sb.AppendLine(v.ToString());
                }
                else
                {
                    sb.AppendLine(v.name + " = {CUSTOM PARSED DATA NOT SUPPORTED}");
                }
                
            }
            foreach (var v in objects)
            {
                StringReader sr = new StringReader(v.ToString());
                while (true)
                {
                    String s = sr.ReadLine();
                    if (s != null)
                        sb.AppendLine("  " + s);
                    else
                        break;
                }
            }
            sb.AppendLine("end");
            return sb.ToString();
        }
    }

    public class DelphiProperty
    {
        public string name;
        public int type;
        public Object value;

        public Type getType()
        {
            switch (type)
            {
                case 0x01:
                    return typeof(Array);
                case 0x02:
                case 0x03:
                case 0x04:
                    return typeof(Int32);
                case 0x05:
                case 0x11:
                    return typeof(Double);
                case 0x06:
                case 0x0C:
                case 0x12:
                    return typeof(String);
                case 0x08:
                case 0x09:
                    return typeof(Boolean);
                case 0x0a:
                    return typeof(Byte[]);
                case 0x0b:
                    return typeof(List<Object>);
                case 0x0d:
                    return typeof(Nullable);
                case 0x0e:
                    return typeof(List<Object>);
                case 0x13:
                    return typeof(Int64);
                default:
                    return null;
            }
        }
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            switch (type)
            {
                case 1:
                    // Array
                    sb.Append("( ");
                    if (value.GetType() != typeof(ArrayList)) throw new InvalidDataException("Non ArrayList in Array Property");
                    foreach (var v in ((ArrayList)value))
                    {
                        if (v.GetType() == typeof(String))
                            sb.Append("'" + v.ToString() + "' ");
                        else
                            sb.Append(v.ToString() + " ");
                    }
                    sb.Append(")");
                    break;
                case 6:
                    sb.Append("'" + value.ToString() + "'");
                    break;
                case 7:
                    sb.Append(value.ToString());
                    break;
                case 0x0A:
                    sb.Append("{");
                    if (value.GetType() != typeof(Byte[])) throw new InvalidDataException("Non Byte Array in Byte Array Property");
                    foreach (var v in ((Byte[])value))
                    {
                        sb.Append(String.Format("{0:X2}", v));
                    }
                    sb.Append("}");
                    break;
                case 0x0B:
                    sb.Append("[ ");
                    if (value.GetType() != typeof(List<String>)) throw new InvalidDataException("Non List<String> in Set Property");
                    foreach (var v in ((List<String>)value))
                    {
                        sb.Append(v.ToString() + ", ");
                    }
                    sb.Append("]");
                    sb.Replace(", ]", " ]");
                    break;
                case 0x0D:
                    sb.Append("nil");
                    break;
                case 0x0E:
                    /*
                     Columns = <
      item
        Expanded = False
        Visible = True
      end
      item
        Expanded = False
        Visible = True
      end>*/
                    sb.Append("< ");
                    if (value.GetType() != typeof(ArrayList)) throw new InvalidDataException("Non ArrayList in Collection Property");

                    foreach (var v in ((ArrayList)value))
                    {
                        sb.Append("item ");
                        foreach (var v1 in ((ArrayList)v))
                        {
                            sb.Append (v1.ToString() + " ");
                        }
                        sb.Append("end ");
                        
                    }
                    sb.Append(">");
                    break;
                case 0x12:
                    sb.Append("''");
                    break;
                default:
                    if (value.GetType() == typeof(String))
                        sb.Append("'" + value.ToString() + "'");
                    else
                        sb.Append(value.ToString());

                    break;
            }
            return "  " + name + " = " + sb.ToString();
        }
    }

    public abstract class DelphiCustomParser
    {
        public abstract Object parse(DelphiParser p);
        public abstract bool shouldParse(String propName);
    }


}
